/***************************************************************************
Name: JSView
Description: Helps you to view the source code of external js and css files
Author: Ron Beckman
Homepage: http://forum.softwareblaze.com

Copyright (C) 2008  Ron Beckman

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to:

Free Software Foundation, Inc.
51 Franklin Street
Fifth Floor
Boston, MA  02110-1301
USA
***************************************************************************/


window.addEventListener("load", jsviewViewSourceLoad, false);

function jsviewViewSourceLoad() {
	jsviewViewSource.init();
}

var jsviewViewSource = {

	_win: null,

	init: function() {
		this._win = this.getContentWin();

		//add popupshowing event to show-hide jsview menu on view menu
		var view;
		var loc = window.location.href;
		if (loc == 'chrome://global/content/viewSource.xul') {
			view = document.getElementById('viewmenu-popup');
		} else {
			view = document.getElementById('menu_View_Popup');
		}
		view.addEventListener('popupshowing', jsviewViewSource.showViewPopup, false);

		//set statusbar icon
		this.load();
	},

	load: function() {
		var status = document.getElementById('jsview-status');
		if (!status) return false;

		var icon,hidden;
		var js = this.getExternalJS();
		var css = this.getExternalCSS();

		if (js.length > 0 && css.length > 0) {
			icon = 'both';
			hidden = 'false';
		} else if (js.length > 0) {
			icon = 'js';
			hidden = 'false';
		} else if (css.length > 0) {
			icon = 'css';
			hidden = 'false';
		} else {
			icon = 'none';
			hidden = 'true';
		}

		var label = this.getStr('jsview.JS') +
				' (' + js.length + ') ' +
				this.getStr('jsview.CSS') +
				' (' + css.length + ')';

		if (status) {
			status.setAttribute('jsview',icon);
			status.setAttribute('tooltiptext',label);
		}
		return false;
	},

	showViewPopup: function() {
		var view = document.getElementById('jsview-view');
		if (!view) return false;

		var icon,hidden;
		var js = jsviewViewSource.getExternalJS();
		var css = jsviewViewSource.getExternalCSS();

		if (js.length > 0 && css.length > 0) {
			icon = 'both';
			hidden = 'false';
		} else if (js.length > 0) {
			icon = 'js';
			hidden = 'false';
		} else if (css.length > 0) {
			icon = 'css';
			hidden = 'false';
		} else {
			icon = 'none';
			hidden = 'true';
		}

		var label = jsviewViewSource.getStr('jsview.JS') +
				' (' + js.length + ') ' +
				jsviewViewSource.getStr('jsview.CSS') +
				' (' + css.length + ')';

		view.setAttribute('jsview',icon);
		view.setAttribute('hidden',hidden);
		view.setAttribute('tooltiptext',label);
		view.setAttribute('label',label);
		return false;
	},

	getAppInfo: function() {
		return Components.classes['@mozilla.org/xre/app-info;1'].
		createInstance(Components.interfaces.nsIXULAppInfo);
	},

	getIOService: function(url) {
		return Components.classes["@mozilla.org/network/io-service;1"].
		getService(Components.interfaces.nsIIOService);
	},

	getAllWindows: function(name) {
		return Components.classes["@mozilla.org/appshell/window-mediator;1"].
		getService(Components.interfaces.nsIWindowMediator).
		getEnumerator(name);
	},

	getWin: function(name) {
		return Components.classes["@mozilla.org/appshell/window-mediator;1"].
		getService(Components.interfaces.nsIWindowMediator).
		getMostRecentWindow(name);
	},

	getContentWin: function() {
		var tab = null;
		var win,brows,loc;
		var url = window.arguments[0].replace(/#.*$/,'');

		var app = this.getAppInfo();
		var wins = this.getAllWindows((app.name!='Songbird') ? 'navigator:browser' : 'Songbird:Main');

		while (!tab && wins.hasMoreElements()) {
			win = wins.getNext();
			brows = win.getBrowser().browsers;
			for (var i=0; i<brows.length; ++i) {
				loc = brows[i].contentWindow.location.href.replace(/#.*$/,'');
				if (loc == url) {
					tab = brows[i].contentWindow;
					break;
				}
			}
		}
		return tab;
	},

	getPrefs: function() {
		return Components.classes["@mozilla.org/preferences-service;1"].
		getService(Components.interfaces.nsIPrefService).
		getBranch("jsview.");
	},

	getCacheService: function() {
		return Components.classes["@mozilla.org/network/cache-service;1"].
		getService(Components.interfaces.nsICacheService).
		createSession("HTTP", 0, true);
	},

	getStr: function(name) {
		return document.getElementById("jsview-bundle").getString(name);
	},

	makeElement: function(attrs) {
		var elem = document.createElement(attrs[0]);
		for (var i=1; i<attrs.length; ++i) {
			elem.setAttribute(attrs[i][0],attrs[i][1]);
		}
		return elem;
	},

	addCommas: function(numOld) {
		var numNew = parseInt(numOld);
		numNew = numNew.toString();
		var rgx = /(\d+)(\d{3})/;
		while (rgx.test(numNew)) {
			numNew = numNew.replace(rgx, '$1' + ',' + '$2');
		}
		return numNew;
	},

	populateMenu: function(menu) {
		while (menu.firstChild) {
			menu.removeChild(menu.firstChild);
		}

		var uri,attrs,fname,cname,size,entry,cacheKey;
		var prefs = this.getPrefs();
		var js = this.getExternalJS();
		var css = this.getExternalCSS();
		var cache = this.getCacheService();
		var filesize = prefs.getBoolPref('fileSizeInKB');

		//get string translations
		var done = this.getStr('jsview.Done');
		var jsnf = this.getStr('jsview.JsNF');
		var cssnf = this.getStr('jsview.CssNF');
		var noname = this.getStr('jsview.NoName');
		var kb = this.getStr('jsview.KB');
		var bytes = this.getStr('jsview.Bytes');
		var jsall = this.getStr('jsview.ViewAllJs');
		var cssall = this.getStr('jsview.ViewAllCss');

		if (js.length < 1) {
			attrs = ['menuitem',
				  ['label',jsnf],
				  ['tooltiptext',jsnf],
				  ['disabled','true'],
				  ['class','menuitem-iconic jsview-none']];
			menu.appendChild(this.makeElement(attrs));
		} else {
			attrs = ['menuitem',
				  ['label',jsall],
				  ['tooltiptext',jsall],
				  ['oncommand',"jsviewViewSource.viewAll('js');"],
				  ['class','menuitem-iconic jsview-js boldNormal']];
			menu.appendChild(this.makeElement(attrs));
		}

		for (var i=0; i<js.length; ++i) {
			//get filename from url
			fname = (js[i].fileName) ? js[i].fileName : noname;

			//add italic for chrome urls
			cname = (js[i].scheme == 'chrome') ? 'jsview-js jsview-italic' : 'jsview-js';

			//get filesize for url if cached
			cacheKey = js[i].spec.replace(/#.*$/,'');
			try {
				entry = cache.openCacheEntry(cacheKey, Components.interfaces.nsICache.ACCESS_READ, false);
				if (filesize) {
					size = ' (' + (entry.dataSize / 1024).toFixed(2) + ' ' + kb + ')';
				} else {
					size = ' (' + this.addCommas(entry.dataSize) + ' ' + bytes + ')';
				}
			} catch(e) {
				size = '';
			}

			attrs = ['menuitem',
				  ['label',fname + size],
				  ['value',js[i].spec],
				  ['statustext',js[i].spec],
				  ['tooltiptext',js[i].spec],
				  ['oncommand','jsviewViewSource.viewSource(this.value,4);'],
				  ['onclick','jsviewViewSource.promptBehavior(event,this.parentNode,this.value);'],
				  ['onmouseover','jsviewViewSource.showStatusText(this.value);'],
				  ['onmouseout',"jsviewViewSource.showStatusText('" + done + "');"],
				  ['class','menuitem-iconic ' + cname]];
			menu.appendChild(this.makeElement(attrs));
		}

		menu.appendChild(this.makeElement(['menuseparator']));

		if (css.length < 1) {
			attrs = ['menuitem',
				  ['label',cssnf],
				  ['tooltiptext',cssnf],
				  ['disabled','true'],
				  ['class','menuitem-iconic jsview-none']];
			menu.appendChild(this.makeElement(attrs));
		} else {
			attrs = ['menuitem',
				  ['label',cssall],
				  ['tooltiptext',cssall],
				  ['oncommand',"jsviewViewSource.viewAll('css');"],
				  ['class','menuitem-iconic jsview-css boldNormal']];
			menu.appendChild(this.makeElement(attrs));
		}

		for (var i=0; i<css.length; ++i) {
			//get filename from url
			fname = (css[i].fileName) ? css[i].fileName : noname;

			//add italic for chrome urls
			cname = (css[i].scheme == 'chrome') ? 'jsview-css jsview-italic' : 'jsview-css';

			//get filesize for url if cached
			cacheKey = css[i].spec.replace(/#.*$/,'');
			try {
				entry = cache.openCacheEntry(cacheKey, Components.interfaces.nsICache.ACCESS_READ, false);
				if (filesize) {
					size = ' (' + (entry.dataSize / 1024).toFixed(2) + ' ' + kb + ')';
				} else {
					size = ' (' + this.addCommas(entry.dataSize) + ' ' + bytes + ')';
				}
			} catch(e) {
				size = '';
			}

			attrs = ['menuitem',
				  ['label',fname + size],
				  ['value',css[i].spec],
				  ['statustext',css[i].spec],
				  ['tooltiptext',css[i].spec],
				  ['oncommand','jsviewViewSource.viewSource(this.value,4);'],
				  ['onclick','jsviewViewSource.promptBehavior(event,this.parentNode,this.value);'],
				  ['onmouseover','jsviewViewSource.showStatusText(this.value);'],
				  ['onmouseout',"jsviewViewSource.showStatusText('" + done + "');"],
				  ['class','menuitem-iconic ' + cname]];
			menu.appendChild(this.makeElement(attrs));
		}
	},

	copyLink: function(url) {
		var brows = this.getAppInfo();
		var win = this.getWin((brows.name!='Songbird') ? 'navigator:browser' : 'Songbird:Main');
		win.jsview.copyLink(url);
	},

	viewAll: function(type) {
		var prefs = this.getPrefs();
		var bhvr = prefs.getIntPref('menuItemsBehavior');
		var uris = (type == 'js') ? this.getExternalJS() : this.getExternalCSS();

		for (var i=0; i<uris.length; ++i) {
			setTimeout('jsviewViewSource.viewSource("'+uris[i].spec+'","'+bhvr+'")',i*500);
		}
	},

	viewSource: function(url,type) {
		var brows = this.getAppInfo();
		var win = this.getWin((brows.name!='Songbird') ? 'navigator:browser' : 'Songbird:Main');
		win.jsview.viewSource(url,type);
	},

	promptBehavior: function(ev,menu,url) {
		if (ev.which > 1) {
			ev.preventDefault();
			menu.hidePopup();
			window.openDialog("chrome://jsview/content/jsviewBehavior.xul", "_blank", "chrome,centerscreen,resizable=yes", url);
		}
	},

	showStatusText: function(txt) {
		document.getElementById('statusbar-line-col').setAttribute('label',txt);
	},

	getFrames: function() {
		if (!this._win) return [];

		var frms = [this._win];

		for (var i=0; i<frms.length; ++i) {
			for (var j=0; j<frms[i].frames.length; ++j) {
				frms.push(frms[i].frames[j]);
			}
			//we break after reaching 50 to avoid a possible endless loop from
			//occurring if the website has coded frames within frames.........
			if (i >= 50) break;
		}
		return frms;
	},

	getExternalJS: function() {
		var scriptUrls = [];
		var prefs = this.getPrefs();
		var io = this.getIOService();
		var scripts,loc,source,srcAttr,uri;

		//get a reference to all dom windows in current page
		var wins = this.getFrames();

		for (var i=0; i<wins.length; ++i) {
			scripts = wins[i].document.getElementsByTagName('script');
			for (var j=0; j<scripts.length; ++j) {
				if (scripts[j].hasAttribute('src')) {
					//use getAttribute for xul script elements
					//use .src for html script elements
					srcAttr = scripts[j].getAttribute('src');
					source = (srcAttr.substr(0,9) == 'chrome://') ? srcAttr : scripts[j].src;
					if (source) {
						//we need to make sure all anchors are removed from the urls
						scriptUrls.push(source.replace(/#.*$/,''));
					}
				}
			}
		}

		//we sort the urls to ensure that chrome urls appear at top of menupopup
		scriptUrls.sort();

		//check for duplicate entries and remove them
		scriptUrls = this.removeDuplicates(scriptUrls);

		//we create the uris here instead of the previous loop since the urls
		//need to be sorted and duplicates removed before the uris can be created
		for (var i=0; i<scriptUrls.length; ++i) {
			try {
				uri = io.newURI(scriptUrls[i],null,null);
				uri.QueryInterface(Components.interfaces.nsIURL);
				scriptUrls[i] = uri;
			} catch(e) {
				scriptUrls.splice(i,1);
				--i;
			}
		}

		//filter the scriptUrls array for unwanted file types
		var exts = prefs.getCharPref('unwantedFileTypes');
		if (exts) {
			scriptUrls = scriptUrls.filter(function(value) {
				return !value.fileExtension || exts.indexOf(' '+value.fileExtension+' ') == -1;
			});
		}

		//filter the scriptUrls array for any urls that match window locations
		for (var i=0; i<wins.length; ++i) {
			loc = wins[i].location.href.replace(/#.*$/,'');
			scriptUrls = scriptUrls.filter(function(value) { return value.spec != loc; });
		}

		//filter the scriptUrls array for chrome urls
		if (!prefs.getBoolPref('showChromeUrls')) {
			scriptUrls = scriptUrls.filter(function(value) { return value.scheme != 'chrome'; });
		}

		return scriptUrls;
	},

	getExternalCSS: function() {
		var sheets,loc,uri;
		var styleUrls = [];
		var prefs = this.getPrefs();
		var io = this.getIOService();

		//get a reference to all dom windows in current page
		var wins = this.getFrames();

		for (var i=0; i<wins.length; ++i) {
			sheets = wins[i].document.styleSheets;
			for (var j=0; j<sheets.length; ++j) {
				if (sheets[j].ownerNode &&
				    sheets[j].ownerNode instanceof Components.interfaces.nsIDOMHTMLStyleElement) {
					//do nothing for embedded stylesheets
				} else if (sheets[j].href) {
					//we need to make sure all anchors are removed from the urls
					styleUrls.push(sheets[j].href.replace(/#.*$/,''));
				}
				styleUrls = styleUrls.concat(this.getImportedCSS(sheets[j]));
			}
		}

		//we sort the urls to ensure that chrome urls appear at top of menupopup
		styleUrls.sort();

		//check for duplicate entries and remove them
		styleUrls = this.removeDuplicates(styleUrls);

		//we create the uris here instead of the previous loop since the urls
		//need to be sorted and duplicates removed before the uris can be created
		for (var i=0; i<styleUrls.length; ++i) {
			try {
				uri = io.newURI(styleUrls[i],null,null);
				uri.QueryInterface(Components.interfaces.nsIURL);
				styleUrls[i] = uri;
			} catch(e) {
				styleUrls.splice(i,1);
				--i;
			}
		}

		//filter the styleUrls array for unwanted file types
		var exts = prefs.getCharPref('unwantedFileTypes');
		if (exts) {
			styleUrls = styleUrls.filter(function(value) {
				return !value.fileExtension || exts.indexOf(' '+value.fileExtension+' ') == -1;
			});
		}

		//filter the styleUrls array for any urls that match window locations
		for (var i=0; i<wins.length; ++i) {
			loc = wins[i].location.href.replace(/#.*$/,'');
			styleUrls = styleUrls.filter(function(value) { return value.spec != loc; });
		}

		//filter the styleUrls array for chrome urls
		if (!prefs.getBoolPref('showChromeUrls')) {
			styleUrls = styleUrls.filter(function(value) { return value.scheme != 'chrome'; });
		}

		return styleUrls;
	},

	getImportedCSS: function(css) {
		var urls = [];
		var sheets = [css];

		for (var i=0; i<sheets.length; ++i) {
			for (var j=0; j<sheets[i].cssRules.length; ++j) {
				if (sheets[i].cssRules[j].type != 3) break;

				if (sheets[i].cssRules[j].styleSheet &&
				    sheets[i].cssRules[j].styleSheet.href) {
					//we need to make sure all anchors are removed from the urls
					urls.push(sheets[i].cssRules[j].styleSheet.href.replace(/#.*$/,''));
					sheets.push(sheets[i].cssRules[j].styleSheet);
				}
			}
			//we break after reaching 50 to avoid a possible endless loop from
			//occurring if a stylesheet tries to import itself over and over...
			if (i >= 50) break;
		}
		return urls;
	},

	removeDuplicates: function(arrOld) {
		var arrNew = [];
		for (var i=0; i<arrOld.length; ++i) {
			if (arrNew.indexOf(arrOld[i]) == -1) {
				arrNew.push(arrOld[i]);
			}
		}
		return arrNew;
	}
};